def preprocess_true_boxes(true_boxes, anchors, image_size):
     """
    参数
    --------------
    true_boxes : 实际框的位置和类别，我们的输入。二个维度：
    第一个维度：一张图片中有几个实际框
    第二个维度： [x, y, w, h, class]，x,y 是框中心点坐标，w,h 是框的宽度和高度。x,y,w,h 均是除以图片分辨率得到的[0,1]范围的比值。
      
    anchors : 实际anchor boxes 的值，论文中使用了五个。[w,h]，都是相对于gird cell 的比值。二个维度：
    第一个维度：anchor boxes的数量，这里是5
    第二个维度：[w,h]，w,h,都是相对于gird cell长宽的比值。[1.08, 1.19], [3.42, 4.41], [6.63, 11.38], [9.42, 5.11], [16.62, 10.52]
        
    image_size : 图片的实际尺寸。这里是416x416。


    Returns
    --------------
    detectors_mask : 取值是0或者1，这里的shape是[13,13,5,1]，四个维度。
    第一个维度：true_boxes的中心位于第几行（y方向上属于第几个gird cell）
    第二个维度：true_boxes的中心位于第几列（x方向上属于第几个gird cell）
    第三个维度：哪个anchor box
    第四个维度：0/1。1的就是用于预测改true boxes 的 anchor boxes

    matching_true_boxes: 这里的shape是[13,13,5,5]，四个维度。
    第一个维度：true_boxes的中心位于第几行（y方向上属于第几个gird cel）
    第二个维度：true_boxes的中心位于第几列（x方向上属于第几个gird cel）
    第三个维度：第几个anchor box
    第四个维度：[x,y,w,h,class]。这里的x，y表示offset，是相当于gird cell的，w,h是取了log函数的，class是属于第几类。后面的代码会详细看到
    """

    height, width = image_size
    num_anchors = len(anchors)

    assert height % 32 == 0,   # 输入的图片的高度必须是32的倍数，不然会报错。
    assert width % 32 == 0,    # 输入的图片的宽度必须是32的倍数，不然会报错。

    conv_height = height // 32    # 进行gird cell划分
    conv_width = width // 32    # 进行gird cell划分

    num_box_params = true_boxes.shape[1] 
    detectors_mask = np.zeros((conv_height, conv_width, num_anchors, 1), dtype=np.float32)
    matching_true_boxes = np.zeros((conv_height, conv_width, num_anchors, num_box_params), dtype=np.float32)    # 确定detectors_mask和matching_true_boxes的维度，用0填充

    for box in true_boxes:    # 遍历实际框
        box_class = box[4:5]    # 提取类别信息，属于哪类

        box = box[0:4] * np.array([conv_width, conv_height, conv_width, conv_height])   # 换算成相对于gird cell的值

        i = np.floor(box[1]).astype(int)    # y方向上属于第几个gird cell
        j = np.floor(box[0]).astype(int)    # x方向上属于第几个gird cell
        best_iou = 0
        best_anchor = 0


        # 计算anchor boxes 和 true boxes的iou，找到anchor boxes中和该true box的iou最大的anchor
        for k, anchor in enumerate(anchors): # 遍历每一个anchor
            # Find IOU between box shifted to origin and anchor box.
            box_maxes = box[2:4] / 2.
            box_mins = -box_maxes
            anchor_maxes = (anchor / 2.)
            anchor_mins = -anchor_maxes

            intersect_mins = np.maximum(box_mins, anchor_mins)
            intersect_maxes = np.minimum(box_maxes, anchor_maxes)
            intersect_wh = np.maximum(intersect_maxes - intersect_mins, 0.)
            intersect_area = intersect_wh[0] * intersect_wh[1]
            box_area = box[2] * box[3]
            anchor_area = anchor[0] * anchor[1]
            iou = intersect_area / (box_area + anchor_area - intersect_area)
            if iou > best_iou:
                best_iou = iou
                best_anchor = k


        if best_iou > 0:
            detectors_mask[i, j, best_anchor] = 1  # 找到的和true box最大的anchor box
            adjusted_box = np.array(
                [
                    box[0] - j, box[1] - i, # x,y都是相对于gird cell的位置，左上角[0,0]，右下角[1,1]
                    np.log(box[2] / anchors[best_anchor][0]),  # 对应实际框w,h和anchor boxes w,h的比值取log函数
                    np.log(box[3] / anchors[best_anchor][1]), box_class  # class实际框的物体是属于第几类
                ],
                dtype=np.float32)
            matching_true_boxes[i, j, best_anchor] = adjusted_box   
    return detectors_mask, matching_true_boxes
    
    





def yolo_head(feats, anchors, num_classes):
    """Convert final layer features to bounding box parameters.

    参数
    ----------
    feats : 神经网络最后一层的输出，shape：[-1,13,13,125]

    anchors : 实际anchor boxes 的值，论文中使用了五个。[w,h]，都是相对于gird cell 长宽的比值。二个维度：
    第一个维度：anchor boxes的数量，这里是5
    第二个维度：[w,h]，w,h,都是相对于gird cell 长宽的比值。[1.08, 1.19], [3.42, 4.41], [6.63, 11.38], [9.42, 5.11], [16.62, 10.52]

    num_classes : 类别个数（有多少类）

    返回值
    -------
    box_xy : 每张图片的每个gird cell中的每个pred_boxes中心点x,y相对于其所在gird cell的坐标值，左上顶点为[0,0],右下顶点为[1,1]。
    有五个维度，shape:[-1,13,13,5,2].
    第一个维度：图片张数
    第二个维度：每组x,y在pred_boxes的行坐标信息（y方向上属于第几个gird cell）
    第三个维度：每组x,y在pred_boxes的列坐标信息（x方向上属于第几个gird cell）
    第四个维度：每组x,y的anchor box信息（使用第几个anchor boxes）
    第五个维度：[x,y],中心点x,y相对于gird cell的坐标值
        
    box_wh : 每张图片的每个gird cell中的每个pred_boxes的w,h都是相对于gird cell的比值
    有五个维度，shape:[-1,13,13,5,2].
    第一个维度：图片张数
    第二个维度：每组w,h对应的x,y在pred_boxes的行坐标信息（y方向上属于第几个gird cell）
    第三个维度：每组w,h对应的x,y在pred_boxes的列坐标信息（x方向上属于第几个gird cell）
    第四个维度：每组w,h对应的x,y的anchor box信息（使用第几个anchor boxes）
    第五个维度：[w,h],w,h都是相对于gird cell的比值

    box_confidence : 每张图片的每个gird cell中的每个pred_boxes的，判断是否存在可检测物体的概率。五个维度，shape:[-1,13,13,5,1]。各维度信息同上。

    box_class_pred : 每张图片的每个gird cell中的每个pred_boxes所框起来的各个类别分别的概率(经过了softmax)。shape:[-1,13,13,5,20]
        
    """
    num_anchors = len(anchors)
    # Reshape to batch, height, width, num_anchors, box_params.
    anchors_tensor = K.reshape(K.variable(anchors), [1, 1, 1, num_anchors, 2])

    conv_dims = K.shape(feats)[1:3]  # 用多少个gird cell划分的图片，这里是13x13
    # In YOLO the height index is the inner most iteration.
    conv_height_index = K.arange(0, stop=conv_dims[0])
    conv_width_index = K.arange(0, stop=conv_dims[1])
    conv_height_index = K.tile(conv_height_index, [conv_dims[1]])

    conv_width_index = K.tile(K.expand_dims(conv_width_index, 0), [conv_dims[0], 1])
    conv_width_index = K.flatten(K.transpose(conv_width_index))
    conv_index = K.transpose(K.stack([conv_height_index, conv_width_index]))
    conv_index = K.reshape(conv_index, [1, conv_dims[0], conv_dims[1], 1, 2])  # shape:[1，13，13，1，2]
    conv_index = K.cast(conv_index, K.dtype(feats))

    '''
    tile（）：平移，
    expand_dims（）：增加维度
    transpose（）：转置
    flatten（）：降成一维
    stack（）：堆积，增加一个维度
    conv_index:[0,0],[0,1],...,[0,12],[1,0],[1,1],...,[12,12]（大概是这个样子）
    '''

    feats = K.reshape(feats, [-1, conv_dims[0], conv_dims[1], num_anchors, num_classes + 5]) # shape: [-1, 13, 13, 5, 25]
    conv_dims = K.cast(K.reshape(conv_dims, [1, 1, 1, 1, 2]), K.dtype(feats))

    box_xy = K.sigmoid(feats[..., :2])          # shape: [-1, 13, 13, 5, 2]
    box_wh = K.exp(feats[..., 2:4])             # shape: [-1, 13, 13, 5, 2]
    box_confidence = K.sigmoid(feats[..., 4:5]) # shape: [-1, 13, 13, 5, 1]
    box_class_probs = K.softmax(feats[..., 5:]) # shape: [-1, 13, 13, 5, 20]

    # Adjust preditions to each spatial grid point and anchor size.
    # Note: YOLO iterates over height index before width index.
    box_xy = (box_xy + conv_index) / conv_dims
    box_wh = box_wh * anchors_tensor / conv_dims

    return box_xy, box_wh, box_confidence, box_class_probs





def yolo_loss(args, anchors, num_classes, rescore_confidence=False, print_loss=False):
    """
    参数
    ----------
    yolo_output : 神经网络最后一层的输出，shape:[batch_size,13,13,125]
        
    true_boxes : 实际框的位置和类别，我们的输入。三个维度：
    第一个维度：图片张数
    第二个维度：一张图片中有几个实际框
    第三个维度： [x, y, w, h, class]，x,y 是实际框的中心点坐标，w,h 是框的宽度和高度。x,y,w,h 均是除以图片分辨率得到的[0,1]范围的值。

    detectors_mask : 取值是0或者1，这里的shape是[ batch_size，13,13,5,1]，其值可参考函数preprocess_true_boxes（）的输出，五个维度：
    第一个维度：图片张数
    第二个维度：true_boxes的中心位于第几行（y方向上属于第几个gird cell）
    第三个维度：true_boxes的中心位于第几列（x方向上属于第几个gird cell）
    第四个维度：哪个anchor box
    第五个维度：0/1。1的就是用于预测改true boxes 的 anchor boxes

    matching_true_boxes :这里的shape是[-1,13,13,5,5]，其值可参考函数preprocess_true_boxes（）的输出，五个维度：
    第一个维度：图片张数
    第二个维度：true_boxes的中心位于第几行（y方向上属于第几个gird cel）
    第三个维度：true_boxes的中心位于第几列（x方向上属于第几个gird cel）
    第四个维度：第几个anchor box
    第五个维度：[x,y,w,h,class]。这里的x，y表示offset，是相当于gird cell的坐标，w,h是取了log函数的，class是属于第几类。

    anchors : 实际anchor boxes 的值，论文中使用了五个。[w,h]，都是相对于gird cell 长宽的比值。二个维度：
    第一个维度：anchor boxes的数量，这里是5
    第二个维度：[w,h]，w,h,都是相对于gird cell 长宽的比值。[1.08, 1.19], [3.42, 4.41], [6.63, 11.38], [9.42, 5.11], [16.62, 10.52]

    num_classes :类别个数（有多少类）

    rescore_confidence : bool值，False和True计算confidence_loss的objects_loss不同，后面代码可以看到。

    print_loss : bool值，是否打印损失，包括总损失，IOU损失，分类损失，坐标损失

    返回值
    -------
    total_loss : float，总损失    
    """
    (yolo_output, true_boxes, detectors_mask, matching_true_boxes) = args
    num_anchors = len(anchors)
    object_scale = 5  # 物体位于gird cell时计算置信度的修正系数
    no_object_scale = 1  # 物体位于gird cell时计算置信度的修正系数
    class_scale = 1   # 计算分类损失的修正系数
    coordinates_scale = 1  # 计算坐标损失的修正系数

    pred_xy, pred_wh, pred_confidence, pred_class_prob = yolo_head(yolo_output, anchors, num_classes)
    # pred_xy           shape: [-1, 13, 13, 5, 2]
    # pred_wh           shape: [-1, 13, 13, 5, 2]
    # pred_confidence   shape: [-1, 13, 13, 5, 1]
    # pred_class_prob   shape: [-1, 13, 13, 5, 20]
    
    yolo_output_shape = K.shape(yolo_output) # [-1, 13, 13, 125]
    feats = K.reshape(yolo_output, [-1, yolo_output_shape[1], yolo_output_shape[2], num_anchors, num_classes + 5])  # shape:[-1, 13, 13, 5, 25]

    # 合并得到pred_boxes的x,y,w,h，用于和matching_true_boxes计算坐标损失,shape:[-1,13,13,5,4]
    pred_boxes = K.concatenate((K.sigmoid(feats[..., 0:2]), feats[..., 2:4]), axis=-1) # shape:[-1,13,13,5,4]
    # Expand pred x,y,w,h to allow comparison with ground truth.
    # batch, conv_height, conv_width, num_anchors, num_true_boxes, box_params
    pred_xy = K.expand_dims(pred_xy, 4)  # 增加一个维度由[-1,13,13,5,2]变成[-1,13,13,5,1,2]
    pred_wh = K.expand_dims(pred_wh, 4)  # 增加一个维度由[-1,13,13,5,2]变成[-1,13,13,5,1,2]
    pred_wh_half = pred_wh / 2.          # shape: [-1,13,13,5,1,2]
    pred_mins = pred_xy - pred_wh_half   # shape: [-1,13,13,5,1,2]
    pred_maxes = pred_xy + pred_wh_half  # shape: [-1,13,13,5,1,2]

    true_boxes_shape = K.shape(true_boxes) # [-1, 一张图片中有几个实际框, 5]
    true_boxes = K.reshape(true_boxes, [true_boxes_shape[0], 1, 1, 1, true_boxes_shape[1], true_boxes_shape[2]]) # shape:[-1,1,1,1,-1,5]
    true_xy = true_boxes[..., 0:2]       # shape: [-1,1,1,1,-1,2]
    true_wh = true_boxes[..., 2:4]       # shape: [-1,1,1,1,-1,2]
    true_wh_half = true_wh / 2.          # shape: [-1,1,1,1,-1,2]
    true_mins = true_xy - true_wh_half   # shape: [-1,1,1,1,-1,2]
    true_maxes = true_xy + true_wh_half  # shape: [-1,1,1,1,-1,2]
    

    # 计算交集
    intersect_mins = K.maximum(pred_mins, true_mins)                 # shape:[-1,13,13,5,-1,2]
    intersect_maxes = K.minimum(pred_maxes, true_maxes)              # shape:[-1,13,13,5,-1,2]
    intersect_wh = K.maximum(intersect_maxes - intersect_mins, 0.)   # shape:[-1,13,13,5,-1,2]
    intersect_areas = intersect_wh[..., 0] * intersect_wh[..., 1]    # shape:[-1,13,13,5,-1]
    # 计算并集
    pred_areas = pred_wh[..., 0] * pred_wh[..., 1]                   # shape:[-1,13,13,5,1]
    true_areas = true_wh[..., 0] * true_wh[..., 1]                   # shape:[-1,1, 1, 1,-1]
    union_areas = pred_areas + true_areas - intersect_areas          # shape:[-1,13,13,5,-1]
    # 计算交并比
    iou_scores = intersect_areas / union_areas  # 计算出所有anchor boxes和true_boxes的IOU，shape:[-1,13,13,5,-1]
    # 选出和每个anchor最大的交并比
    best_ious = K.max(iou_scores, axis=4)  # shape: [-1,13,13,5]这里很有意思，若两个true_boxes落在同一个gird cell里，我只取iou最大的那一个，因为best_iou这个值只关心在这个gird cell中最大的那个iou，不关心来自于哪个true_boxes。
    best_ious = K.expand_dims(best_ious)   # shape: [-1,13,13,5,1]
    
    
    # 选出IOU大于0.6的，不关注其损失。cast（）函数，第一个参数是bool值，dtype是int，就会转换成0,1
    object_detections = K.cast(best_ious > 0.6, K.dtype(best_ious))  # shape:[-1,13,13,5,1]
    
    # (1-detectors_mask)表示该anchor本应该是负责预测背景的(没有object)；(1-object_detections)表示该anchor和ture_boxes的iou小于0.6，确实是当做预测背景(没有object)来算；
    no_object_weights = (no_object_scale * (1 - object_detections) * (1 - detectors_mask)) # 标量*[-1,13,13,5,1]*[-1,13,13,5,1] = [-1,13,13,5,1]
    no_objects_loss = no_object_weights * K.square(-pred_confidence)                       # [-1,13,13,5,1]*[-1,13,13,5,1] = [-1,13,13,5,1]
    
    # 
    if rescore_confidence:
        # detectors_mask表示该anchor应该是有object的；有对象的置信度用best_ious表示
        objects_loss = (object_scale * detectors_mask * K.square(best_ious - pred_confidence)) # 标量*[-1,13,13,5,1]*([-1,13,13,5,1]-[-1,13,13,5,1]) = [-1,13,13,5,1]
    else:
        # detectors_mask表示该anchor应该是有object的；有对象的置信度用1表示
        objects_loss = (object_scale * detectors_mask * K.square(1 - pred_confidence))         # 标量*[-1,13,13,5,1]*(标量-[-1,13,13,5,1]) = [-1,13,13,5,1]
        
    # 计算confidence_loss，no_objects_loss是计算background的误差， objects_loss是计算与true_box匹配的anchor_boxes的误差，相比较no_objects_loss更关注这部分误差，其修正系数为5
    confidence_loss = objects_loss + no_objects_loss    # shape:[-1,13,13,5,1]


    # 计算classification_loss，20维向量的差
    matching_classes = K.cast(matching_true_boxes[..., 4], 'int32')  # shape:[-1,13,13,5,5]-->[-1,13,13,5,1]，相当于将最后一维中的类别值挑了出来
    matching_classes = K.one_hot(matching_classes, num_classes)      # shape:[-1,13,13,5,20]，相当于将最后一维的类别值变成了one_hot
    classification_loss = (class_scale * detectors_mask * K.square(matching_classes - pred_class_prob))   # shape:[-1,13,13,5,20]
    
    # 计算coordinates_loss， x,y都是offset的均方损失，w,h是取了对数的均方损失，与YOLOv1中的平方根的差的均方类似，效果比其略好一点
    matching_boxes = matching_true_boxes[..., 0:4]                   # shape:[-1,13,13,5,5]-->[-1,13,13,5,4]，相当于将最后一维中的位置值挑了出来
    coordinates_loss = (coordinates_scale * detectors_mask * K.square(matching_boxes - pred_boxes))       # shape:[-1,13,13,5,4]

    confidence_loss_sum = K.sum(confidence_loss)          # [-1,13,13,5,1] --> 标量
    classification_loss_sum = K.sum(classification_loss)  # [-1,13,13,5,20]--> 标量
    coordinates_loss_sum = K.sum(coordinates_loss)        # [-1,13,13,5,4] --> 标量
    total_loss = 0.5 * (confidence_loss_sum + classification_loss_sum + coordinates_loss_sum)
    if print_loss:
        total_loss = tf.Print(
            total_loss, [
                total_loss, confidence_loss_sum, classification_loss_sum,
                coordinates_loss_sum
            ],
            message='yolo_loss, conf_loss, class_loss, box_coord_loss:')

    return total_loss


# 将yolo_head（）函数输出的的x,y作为输入，求出该boxes的左上顶点和右下顶点，作为yolo_filter_boxes（）的输入，可用于画出bounding box
def yolo_boxes_to_corners(box_xy, box_wh):
    box_mins = box_xy - (box_wh / 2.)
    box_maxes = box_xy + (box_wh / 2.)
    return K.concatenate([
        box_mins[..., 1:2],  # y_min
        box_mins[..., 0:1],  # x_min
        box_maxes[..., 1:2],  # y_max
        box_maxes[..., 0:1]  # x_max
    ])
    
    
# 从845个 pred_boxes中选出置信度大于0.6的作为最终的predict bounding boxes
def yolo_filter_boxes(boxes, box_confidence, box_class_probs, threshold=.6):
    box_scores = box_confidence * box_class_probs    # 定义一个box_scores，就是该 bounding boxes的置信度。shape:[-1,13,13,5,20]
    box_classes = K.argmax(box_scores, axis=-1)      # 求出最大box_scores的索引，即属于第几类
    box_class_scores = K.max(box_scores, axis=-1)    # 求出最大box_scores的值，作为bounding boxes的置信度
    prediction_mask = box_class_scores >= threshold  # 选出box_scores大于设定阈值的anchor_boxes。bool值，配合tf.boolean_mask（）函数获取True所在位置的值
    boxes = tf.boolean_mask(boxes, prediction_mask)  # 符合要求的bounding boxes
    scores = tf.boolean_mask(box_class_scores, prediction_mask)  # 其对应的置信度
    classes = tf.boolean_mask(box_classes, prediction_mask)   # 其对应的分类结果
    return boxes, scores, classes

# 其中嵌套使用了yolo_boxes_to_corners（）函数和yolo_filter_boxes（）函数，
# 然后对使用了置信度筛选后的bounding boxes使用了非极大值抑制输出 boxes, scores, classes，
# 分别是bounding boxes的左上顶点和右下顶点的坐标，bounding boxes的置信度，bounding boxes的的分类类别。
def yolo_eval(yolo_outputs, image_shape, max_boxes=10, score_threshold=.6, iou_threshold=.5):
    box_xy, box_wh, box_confidence, box_class_probs = yolo_outputs  # yolo_outputs是yolo_head的输出
    boxes = yolo_boxes_to_corners(box_xy, box_wh)
    boxes, scores, classes = yolo_filter_boxes(boxes, box_confidence, box_class_probs, threshold=score_threshold)
    # image_shape,(416x416)
    height = image_shape[0]  
    width = image_shape[1]
    image_dims = K.stack([height, width, height, width])
    image_dims = K.reshape(image_dims, [1, 4])
    boxes = boxes * image_dims  # 乘以图片分辨率，得到真实的x,y,w,h
    # 运行一下NMS，非极大值抑制，iou_threshold默认是0.5，在训练时实际取了0.9，但是这里没有分类别使用，我猜测这也是提高阈值的原因吧
    max_boxes_tensor = K.variable(max_boxes, dtype='int32')
    K.get_session().run(tf.variables_initializer([max_boxes_tensor]))
    nms_index = tf.image.non_max_suppression(boxes, scores, max_boxes_tensor, iou_threshold=iou_threshold)
    boxes = K.gather(boxes, nms_index)  # gather()函数，获取索引对应的值
    scores = K.gather(scores, nms_index)
    classes = K.gather(classes, nms_index)
    return boxes, scores, classes
